package net;

import exceptions.TransferCollapsedException;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import interfaces.Core_Events;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;

/**
 *
 * @author Groš
 */

/** First part logic for sending/receiving via UDP */
public class UDP_Core {
   
    /** Acknoledge values - <code>FIN = -1; ERR = -2</code> */
    protected static enum ACK { FIN(-1), ERR(-2);
            
            private int value;
            private ACK(int value) { this.value = value; }
            public int value() { return this.value; }
    };
    
    private Core_Events event;
    
    /** clients buffered input stream - reads data from a file */
    private BufferedInputStream client_bis;
    
    private DatagramSocket client_socket;
    private DatagramPacket client_packet;
    
    /** server buffered output stream - write data to the file */
    private BufferedOutputStream server_bos;
    private DatagramSocket server_socket;
    
    /** <code>false</code>  = stop listening */
    private boolean server_state = false;
    
    /** where to listen */
    private String server_ip;
    
    /** on which port listen */
    private int server_port;  
    
    /** <code>false</code> = stop sending data */
    private boolean client_state = false;
    
    /** where to send data from a client side */
    private String client_remoteIP; 
    
    /** on which port to send data from a client side */
    private int client_remotePort;
    
    /** which file is used to read from*/
    private String client_filePath;
    
    private Thread server_thread;
    private Thread client_thread;
    
    /** client logic
     * @see net.UDP_Client#UDP_Client(java.net.DatagramSocket, interfaces.Core_Events) 
     */
    private UDP_Client udp_c;
    
    /** server logic
     * @see net.UDP_Server#UDP_Server(java.net.DatagramSocket, interfaces.Core_Events) 
     */
    private UDP_Server udp_s;
    
    /** saves file name received from client */      
    private String server_fileName;
    
    /** saves file length received from client */
    private String server_fileLength;
    
    /** <code>true</code> = start writing data to the file */
    private boolean server_receivingData = false;
    
    /** <code>true</code> = changes to listen mode, wait for the firts packet from a client
     *  <code>false</code> = receives data from a client
     */
    private boolean server_transferOK = false;
    
    /** First part logic for sending/receiving data via UDP. Needs <code>Core_Event</code> interface
     * @param event interface
     */
    public UDP_Core(Core_Events event) {
        this.event = event;
    }

    /** main code processing when client thread starts
     *  start sending file - First packet - "N:fileName"
     *                       Second packet- "L:fileLength" 
     *                       ..... packet - raw file data 
     */
    public void client_send_thread () 
    {
        File file = new File (client_filePath);
        
        // checks the file
        if (! file.exists()) {
            event.core_message("UDP:E>File not found: " + client_filePath);
            return;
        }
        
        try { 
            client_socket = new DatagramSocket ();
            client_socket.setSoTimeout(core.Core.SOCKET_TIMEOUT);
        }
        
        catch (SocketException sex) {
            event.core_message("UDP:E>" + sex.getLocalizedMessage());
            return;
        }
        
        byte[] bytesFile = new byte[core.Core.DATAGRAM_LENGTH - 4];
        byte[] bytesPacket = new byte[core.Core.DATAGRAM_LENGTH];
        
        // asociates packet id to packets
        int datagramCounter = 0;
        int dataLength = 0;
                
        client_packet = new DatagramPacket(bytesPacket, core.Core.DATAGRAM_LENGTH);
        
        // setting destination IP and port
        try {
            client_packet.setAddress(InetAddress.getByName(client_remoteIP));
            client_packet.setPort(client_remotePort);
        }
        catch (UnknownHostException uhex) {
            event.core_message("UDP:E>" + uhex.getLocalizedMessage());
            return;
        }

        try {
            client_bis = new BufferedInputStream (new FileInputStream (file));
            udp_c = new UDP_Client(client_socket, event);

            // send file name
            client_packet.setData(udp_joinByteArrays(udp_intToBytes(++datagramCounter), new String("N:" + file.getName()).getBytes()));
            udp_c.sendPacket(client_packet, false);
            
            // send file length
            client_packet.setData(udp_joinByteArrays(udp_intToBytes(++datagramCounter), new String("L:" + file.length()).getBytes()));
            udp_c.sendPacket(client_packet, false);
            
            // send raw file data
            while((dataLength = client_bis.read(bytesFile)) != -1 && client_state) {
                ++datagramCounter;
                client_packet.setData(udp_joinByteArrays(udp_intToBytes(datagramCounter), bytesFile));
                client_packet.setLength(4 + dataLength);
                
                if (dataLength == 124) // we are not at the end of file
                    udp_c.sendPacket(client_packet, false);
                else 
                    // must be flushed before terminating packet sent
                    udp_c.sendPacket(client_packet, true);
            }
            
            
            // send ACK transfer finished
            client_packet.setData(udp_intToBytes(ACK.FIN.value()));
            udp_c.sendPacket(client_packet, true);
                        
        }
        catch (SocketException stex) { event.core_message("UDP:E>"+stex.getLocalizedMessage());}
        catch (IOException ioex) { event.core_message("UDP:E>" + ioex.getLocalizedMessage()); }
        catch (TransferCollapsedException tcex) { event.core_message("UDP:C>" + tcex.getLocalizedMessage());}

        datagramCounter = 0;
        client_socket.close();
        client_closeFileRead();
    }
    
    /** main code processing when server thread starts
     *  receives and process packet
     */
    public void server_listen_thread() 
    {   
        try {
                server_socket = new DatagramSocket(server_port, InetAddress.getByName(server_ip));
                server_socket.setSoTimeout(core.Core.SOCKET_TIMEOUT);
        }
        catch (SocketException sex) { 
            event.core_message("UDP:E>" + sex.getLocalizedMessage());
            return;
        }
        catch (UnknownHostException uhex){
            event.core_message("UDP:E>" + uhex.getLocalizedMessage());
            return;
        }
        
        udp_s = new UDP_Server(server_socket, event);
        
        event.core_message("UDP:S>Start listening on port= " + server_socket.getLocalPort());
        
        // loop for receiving
        while (server_state) {
            try { 
                // try to received packets
                udp_s.receivePackets();
                
                // do something with packets
                server_processPackets();
                
                // if anything else is going to be received
                if (!server_transferOK) {
                    
                    // make free space in a receiving buffer
                    udp_s.moveBuffer(udp_s.getSavePosition());
                }
                else {  // if all data received
                    // reset server
                    udp_s.reset();
                    
                    // reset output stream
                    server_bos = null;
                    event.core_message("UDP:S>Transfer finished");
                    
                    // change state to listening mode
                    server_transferOK = false;
                }
           }
            
           catch (SocketTimeoutException stoex) { continue; }
           catch (SocketException stex) { 
               event.core_message("UDP:E>" + stex.getLocalizedMessage()); 
               break;
           }
           catch (IOException ioex) { 
               event.core_message("UDP:E>" + ioex.getLocalizedMessage());
               break;
           }
           catch(TransferCollapsedException tcex) { 
               event.core_message("UDP:S>" + tcex.getLocalizedMessage());
               break;
           }
        }
        
        server_socket.close();
        server_closeFileOutput();
        event.core_message("UDP:S>Listening stopped");
    }
    
    /** creates and runs server thread for receiving 
     * @param IP where to listen
     * @param port where to listen
     * @see UDP_Core#server_listen_thread() 
     */
    public void set_server_listen(String IP, int port) {
        this.server_state = true;
        this.server_ip = IP;
        this.server_port = port;
             
        server_thread = new Thread("server_thread")
        {
            @Override
            public void run() { server_listen_thread(); }
        };
        server_thread.start();
        
    }
    
    /** softly stops server thread  */
    public void set_server_stop() {
        if (udp_s != null) udp_s.setStop(true);
        this.server_state = false;
        
        try { server_thread.join(); }
        catch (InterruptedException iex) {
           event.core_message("UDP:E>" + iex.getLocalizedMessage());
        }
    }
    
    /** creates and runs client thread for sendind 
     * @param IP remote address for sending
     * @param port remote port for sending
     * @param filePath file choosed to send
     * @see UDP_Core#client_send_thread() 
     */
    public void set_client_send(String IP, int port, String filePath){
        this.client_state = true;
        this.client_remoteIP = IP;
        this.client_remotePort = port;
        this.client_filePath = filePath;
        
        client_thread = new Thread("client_thread")
        {
            @Override
            public void run(){ client_send_thread(); }
        };
        client_thread.start();
    }
    
    /** softly stops client thread */
    public void set_client_stop() {
        if (udp_c != null) udp_c.setStop(true);
        this.client_state = false;
        
        try { client_thread.join(); }
        catch (InterruptedException iex) {
            event.core_message("UDP:E>" + iex.getLocalizedMessage());
        }
    }
    
    /** process packets received from client 
     * @throws IOException
     */
    private void server_processPackets() throws IOException{
        String pData;
        int pID;
        
        // for every well received packets
        for (int i=0; i<udp_s.getSavePosition(); i++) {
            
            // find out packet ID
            pID = udp_getPacketID(udp_s.getPackets()[i]);
            
            // if packet ID = 1 or = 2 - packets with file name or file lengt received
            // find out packet data
            pData = (pID == 1 || pID ==2) ? new String(udp_getPacketData(udp_s.getPackets()[i])) : null;
            
            server_transferOK = false;
            
            // if finishing acknowledge received - close outputs, change server state
            if ( pID == ACK.FIN.value()) { 
                server_receivingData = false;
                server_transferOK = true;
                server_closeFileOutput();
                break;
            }
              
            // packet ID = 1 - find out file name
            if (pID == 1 && !server_receivingData) {
                    server_fileName = core.Core.core_parse(pData, ":", 2);
                    server_receivingData = false;
                    event.core_message("UDP:S>Received file-name=" + server_fileName);
                    continue;
            }
            
            // packet ID = 2 - find out file length and change state to write file data
            if (pID == 2 && !server_receivingData) {
                server_fileLength = core.Core.core_parse(pData, ":", 2);
                server_receivingData = true;   
                event.core_message("UDP:S>Received file-length=" + server_fileLength);
                continue;
            }
            
            // if receivinng file data - write them to the file
            if (server_receivingData) 
                server_saveFile(udp_getPacketData(udp_s.getPackets()[i]));
                
        }
    }
      
    /** write data to the file 
     * @param sData data to write
     * @throws java.io.IOException
     */
    private void server_saveFile(byte[] sData) throws IOException{
        if (server_bos == null)
            server_bos = new BufferedOutputStream(new FileOutputStream (new File(core.Core.DOWNLOADS_DIRECTORY + server_fileName), false));
        server_bos.write(sData,0,sData.length);
        server_bos.flush();
    }
    
    /** closes output stream */
    private void server_closeFileOutput() {
        if (server_bos != null)
            try { server_bos.close(); }
            catch (IOException ioex) { event.core_message("UDP:E>" + ioex.getLocalizedMessage()); }
    }
    
    /** closes input stream */
    private void client_closeFileRead(){
        if (client_bis != null)
            try { client_bis.close(); }
            catch (IOException ioex) { event.core_message("UDP:E>" + ioex.getLocalizedMessage()); }
    }
    
    /** converts bytes to String representation (HEX) <code>AE OF 3C ...</code>
    *  @param b bytes to convert
    *  @return String
    */
   protected static String udp_bytesToString (byte[] b) {
       String s = "";
       for (int i = 0; i < b.length; ++i) 
           s += Integer.toHexString(( b[i] & 0xFF ) | 0x100).substring(1,3) + " ";
       return s.toUpperCase();
   }    
   
   /** converts integer to bytearray 
    * @param i int to convert
    * @return byte[] converted int
    */
   protected static byte[] udp_intToBytes(int i) {
       ByteBuffer buf = ByteBuffer.allocate(4);
       buf.putInt(i);
       return buf.array();
   }
   
   /** converts bytearray to int
    * @param b bytearry to convert
    * @return int converted bytearray
    */
   protected static int udp_bytesToInt(byte[] b) {
       ByteBuffer buf = ByteBuffer.wrap(b);
       return buf.getInt();
   }
   
   /** find out packet ID
    * @param p source packet
    * @return packet ID
    */
   protected static int udp_getPacketID (DatagramPacket p) {
       byte[] hexID = new byte[4];
       for (int i=0; i<4; i++)
           hexID[i] = p.getData()[i];
       return udp_bytesToInt(hexID);
   }
   
   /** find out packet data (without <code> Packet ID</code>)
    * @param p source packet
    * @return packet data
    */
   protected static byte[] udp_getPacketData(DatagramPacket p) {
        byte[] pData = new byte[p.getLength() - 4];
        for(int i=4; i < p.getLength(); i++)
           pData[i-4] = p.getData()[i];
        return pData;
   }
   
   /** joins 2 bytearrays
    * @param source1
    * @param source2
    * @return joined bytearray
    */
   protected static byte[] udp_joinByteArrays (byte[] source1, byte[] source2) {
       byte[] b = new byte[source1.length + source2.length];
       
       for (int i=0; i<b.length; i++) 
           b[i] = (i < source1.length) ? source1[i] : source2[i - source1.length];
       return b;
   }
}
